<?php

namespace WDB\Annotation;
use WDB;

/**
 * Annotation reader.
 * 
 * Can read annotations from a string or any php reflection class supporting getDocComment().
 *
 * @author Richard Ejem <richard(at)ejem.cz>
 * @packade WDB
 * @property-read string $text
 */
class Reflection
{

    protected $doc = '';
    protected $text = '';
    protected $tags = array();

    protected function __construct(){}
    
    /**
     * Factory method - creates annotation reflection from an object supporting getDocComment().
     *
     * @param object any (reflection) object supporting getDocComment() method
     * @return Reflection
     * 
     * @throws WDB\Exception\BadArgument
     */
    public static function fromReflObject($obj)
    {
        return Reflection::_fromReflObject($obj, __CLASS__);
    }
    
    protected static function _fromReflObject($obj, $class)
    {
        if (!method_exists($obj, 'getDocComment'))
        {
            throw new WDB\Exception\BadArgument("Cannot obtain doc comment from object without support of getDocComment() method.");
        }
        $ar = new $class();
        $ar->parse($obj->getDocComment());
        return $ar;
    }
    
    /**
     * Factory method - creates annotation reflection from raw string.
     *
     * @param string annotation comment
     * @return self
     */
    public static function fromString($str)
    {
        return Reflection::_fromString($str, __CLASS__);
    }
    
    protected static function _fromString($str, $class)
    {
        $ar = new $class();
        $ar->parse($str);
        return $ar;
    }
    
    /**
     * Merge current object with additional annotations. Only input string supported.
     *
     * @param string $str
     */
    public function merge($str)
    {
        $this->parse($str);
    }
    
    /**
     * Merge current object with NeonWheel configuration
     *
     * @param array $neonWheel
     */
    public function mergeNeonWheel($neonWheel, $key = NULL)
    {
        if ($key != NULL)
        {
            $key = explode('.', $key);
            foreach ($key as $keypart)
            {
                if (!isset($neonWheel[$keypart])) return;
                $neonWheel = $neonWheel[$keypart];
            }
        }
        foreach ($neonWheel as $tagname=>$tagdata)
        {
            if (is_array($tagdata)) continue; //sub-keys are not annotations for this level
            if (strlen($tagname) > 0 && $tagname{0} == '@')
            {
                $tagname = substr($tagname, 1);
            }
            $tagname = strtolower($tagname);
            $parsed = $this->parseTag($tagname, $tagdata);
            
            if (!isset($this->tags[$tagname]))
            {
                $this->tags[$tagname] = array();
            }
            $this->tags[$tagname][] = $parsed;
        }
    }

    
    /**
     * Parse string into this object - fills doc, tags and text properties.
     *
     * @param string comment block
     */
    protected function parse($comment)
    {
        $this->doc .= $comment;
        //remove leading and ending /** */
        $stripped = preg_replace('~^/\\*\\*(.*)\\*/$~sm', '\\1', $comment);
        //split lines
        $lines = explode("\n", str_replace(array("\r\n", "\r"), "\n", $stripped));
        
        $parsed = NULL;
        foreach ($lines as $line)
        {
            //remove leading stars and ending whitespace chars
            $line = trim(preg_replace('~^\\s*\\*\\s*(.*?)\\s*~', '\\1', trim($line)));
            //empty line
            if ($line == '')
                continue;
            if (preg_match('~^@(?<name>[0-9a-z_-]+)(\\s(?<data>.*))?~i', $line, $matches)) //tag line
            {
                do
                {
                    $tagname = strtolower($matches['name']);
                    $tagdata = isset($matches['data']) ? $matches['data'] : NULL;
                    if ($p = preg_match('~^(?<tagdata>.*?)\\s@(?<name>[0-9a-z_-]+)(\\s(?<data>.*))?~i', $tagdata, $matches)) //multiple tags on line
                    {
                        $tagdata = $matches['tagdata'];
                    }
                    
                    if (!isset($this->tags[$tagname]))
                    {
                        $this->tags[$tagname] = array();
                    }
                    $parsed = $this->parseTag($tagname, $tagdata);
                    $this->tags[$tagname][] = $parsed;
                } while ($p);
            } else //text or tag continue line
            {
                if ($parsed !== NULL)
                {
                    $parsed->appendLine($line);
                } else
                {
                    $this->text .= $line;
                }
            }
        }
    }

    /**
     *
     * @param string $name
     * @param type $data
     * @return Property
     */
    protected function parseTag(&$name, $data)
    {
        return new Data($data);
    }
    
    /**
     * returns array of annotations with specified key.
     *
     * @param string $key
     * @return Data[]
     */
    public function __get($key)
    {
        $key = strtolower($key);
        if ($key == 'text') return $this->text;
        if (isset($this->tags[$key])) return $this->tags[$key];
        return array();
    }
    
    /**
     * verifies if annotation key exist.
     * 
     * @param string $key
     * @return bool
     */
    public function __isset($key)
    {
        return isset($this->tags[strtolower($key)]);
    }
    
    /**
     * returns first annotation value with the specified key.
     *
     * @param string $key
     * @return Data|NULL
     */
    public function first($key)
    {
        
        if (count($this->$key) == 0) return null;
        return $this->{$key}[0];
    }
    
    /**
     * returns first annotation value with the specified key.
     *
     * @param string $key
     * @return Data|NULL
     */
    public function last($key)
    {
        if (count($this->$key) == 0) return null;
        return WDB\Utils\Arrays::last($this->$key);
    }
}